﻿//---------------------------------------------------------------------
// <copyright file="SaveChangesSingleRequestCalculator.cs" company="Microsoft">
//      Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

namespace Microsoft.Test.Taupo.Astoria.Client
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using Microsoft.Test.Taupo.Astoria.Common;
    using Microsoft.Test.Taupo.Astoria.Contracts;
    using Microsoft.Test.Taupo.Astoria.Contracts.Client;
    using Microsoft.Test.Taupo.Astoria.Contracts.EntityModel;
    using Microsoft.Test.Taupo.Astoria.Contracts.Http;
    using Microsoft.Test.Taupo.Astoria.Contracts.OData;
    using Microsoft.Test.Taupo.Astoria.Contracts.Product;
    using Microsoft.Test.Taupo.Common;
    using Microsoft.Test.Taupo.Contracts;
    using Microsoft.Test.Taupo.Contracts.EntityModel;
    using Microsoft.Test.Taupo.Execution;

    /// <summary>
    /// Default implementation of the save changes single request calculator
    /// </summary>
    [ImplementationName(typeof(ISaveChangesSingleRequestCalculator), "Default")]
    public class SaveChangesSingleRequestCalculator : ISaveChangesSingleRequestCalculator
    {
        private const string DefaultEncoding = "UTF-8";
        private const string DefaultAccept = MimeTypes.ApplicationAtomXml + "," + MimeTypes.ApplicationXml;

        /// <summary>
        /// The names of the headers that can be generated by this component. Each expected request will either have a specific value
        /// expected for the header, or it will be black-listed (ie, null)
        /// </summary>
        private static readonly string[] headersThatWillBeGenerated = new string[] 
        {
            HttpHeaders.DataServiceVersion, 
            HttpHeaders.MaxDataServiceVersion,
            HttpHeaders.Prefer, 
            HttpHeaders.IfMatch,
            HttpHeaders.Accept,
            HttpHeaders.AcceptCharset,
            HttpHeaders.ContentType,
        };

        /// <summary>
        /// Gets or sets the value indicating the value of the "Accept" header of the client requests. If left empty, the value set by the product will not be overriden.
        /// </summary>
        [InjectTestParameter("ClientRequestAcceptHeader", DefaultValueDescription = "Empty string", HelpText = "Value to set for 'Accept' header of requests")]
        public string ClientRequestAcceptHeader { get; set; }

        /// <summary>
        /// Gets or sets the entity descriptor value calculator
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IEntityDescriptorValueCalculator EntityDescriptorValueCalculator { get; set; }

        /// <summary>
        /// Gets or sets the xml to payload element converter
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IXmlToPayloadElementConverter XmlToPayloadElementConverter { get; set; }

        /// <summary>
        /// Gets or sets the entity model schema.
        /// </summary>
        /// <value>The entity model schema.</value>
        [InjectDependency(IsRequired = true)]
        public EntityModelSchema ModelSchema { get; set; }

        /// <summary>
        /// Gets or sets the xml primitive converter to use
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IXmlPrimitiveConverter XmlPrimitiveConverter { get; set; }

        /// <summary>
        /// Gets or sets the version calculator to use
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IEntityDescriptorVersionCalculator VersionCalculator { get; set; }

        /// <summary>
        /// Gets or sets the payload builder to use
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IODataPayloadBuilder PayloadBuilder { get; set; }

        /// <summary>
        /// Calculates expected data for a request during DataServiceContext.SaveChanges for a particular descriptor.
        /// </summary>
        /// <param name="contextData">The context data</param>
        /// <param name="propertyValuesBeforeSave">The property values of the tracked client objects before the call to SaveChanges</param>
        /// <param name="descriptorData">The descriptor data</param>
        /// <param name="options">The save changes options</param>
        /// <returns>The expected client request</returns>
        public ExpectedClientRequest CalculateRequest(DataServiceContextData contextData, IDictionary<object, IEnumerable<NamedValue>> propertyValuesBeforeSave, DescriptorData descriptorData, SaveChangesOptions options)
        {
            ExceptionUtilities.CheckArgumentNotNull(contextData, "contextData");
            ExceptionUtilities.CheckArgumentNotNull(descriptorData, "descriptorData");
            ExceptionUtilities.CheckArgumentNotNull(propertyValuesBeforeSave, "propertyValuesBeforeSave");

            var linkDescriptorData = descriptorData as LinkDescriptorData;
            var entityDescriptorData = descriptorData as EntityDescriptorData;
            var streamDescriptorData = descriptorData as StreamDescriptorData;

            ExpectedClientRequest request = null;
            if (linkDescriptorData != null)
            {
                if (linkDescriptorData.WillTriggerSeparateRequest())
                {
                    request = this.CreateLinkRequest(linkDescriptorData, options);
                }
            }
            else if (entityDescriptorData != null)
            {
                if (entityDescriptorData.State == EntityStates.Added)
                {
                    request = this.CreateEntityInsertRequest(contextData, propertyValuesBeforeSave, entityDescriptorData, options);
                }
                else if (entityDescriptorData.State == EntityStates.Modified)
                {
                    request = this.CreateEntityUpdateRequest(contextData, propertyValuesBeforeSave, entityDescriptorData, options);
                }
                else if (entityDescriptorData.State == EntityStates.Deleted)
                {
                    request = this.CreateEntityDeleteRequest(entityDescriptorData, options);
                }
            }
            else if (streamDescriptorData != null)
            {
                if (streamDescriptorData.State == EntityStates.Added)
                {
                    request = this.CreateStreamInsertRequest(contextData, streamDescriptorData, options);
                }
                else if (streamDescriptorData.State == EntityStates.Modified)
                {
                    request = this.CreateStreamUpdateRequest(streamDescriptorData, options);
                }
            }

            if (request != null)
            {
                request.Headers[HttpHeaders.MaxDataServiceVersion] = ToClientHeaderFormat(contextData.MaxProtocolVersion);

                // perform sanity checks
                var missingHeaders = headersThatWillBeGenerated.Where(h => !request.Headers.ContainsKey(h)).ToArray();
                ExceptionUtilities.Assert(missingHeaders.Length == 0, "Generated request was missing headers: {0}", string.Join(", ", missingHeaders));
                ExceptionUtilities.CheckObjectNotNull(request.Uri, "Generated request was missing a Uri");

                // sanity check: Client sends content-type header for delete request
                if (request.GetEffectiveVerb() == HttpVerb.Delete)
                {
                    ExceptionUtilities.Assert(
                        request.Headers[HttpHeaders.ContentType] == null,
                        "Incorrect expectation: client should never send ContentType header for DELETE requests.");
                }
            }

            return request;
        }

        internal static HttpVerb GetUpdateVerb(SaveChangesOptions options)
        {
            if (options == SaveChangesOptions.ReplaceOnUpdate)
            {
                return HttpVerb.Put;
            }
            else if (options == SaveChangesOptions.PatchOnUpdate)
            {
                return HttpVerb.Patch;
            }
            else
            {
                return HttpVerb.Patch;
            }
        }

        internal static DataServiceProtocolVersion GetDataServiceVersion(HttpVerb verb, string preferHeader)
        {
            var version = DataServiceProtocolVersion.V4;
            if (verb == HttpVerb.Patch || preferHeader != null)
            {
                version = version.IncreaseVersionIfRequired(DataServiceProtocolVersion.V4);
            }

            return version;
        }

        internal static Uri GetEntityInsertUri(DataServiceContextData contextData, EntityDescriptorData entityDescriptorData)
        {
            Uri insertUri;
            if (entityDescriptorData.InsertLink != null)
            {
                insertUri = entityDescriptorData.InsertLink;
            }
            else
            {
                ExceptionUtilities.CheckObjectNotNull(entityDescriptorData.ParentForInsert, "Entity descriptor data did not have insert link or parent for insert: {0}", entityDescriptorData);
                ExceptionUtilities.CheckObjectNotNull(entityDescriptorData.ParentPropertyForInsert, "Entity descriptor data did not have insert link or parent property for insert: {0}", entityDescriptorData);

                var parentDescriptor = contextData.GetEntityDescriptorData(entityDescriptorData.ParentForInsert);
                var linkInfo = parentDescriptor.LinkInfos.SingleOrDefault(l => l.Name == entityDescriptorData.ParentPropertyForInsert);

                if (linkInfo != null && linkInfo.NavigationLink != null)
                {
                    insertUri = linkInfo.NavigationLink;
                }
                else
                {
                    insertUri = new Uri(UriHelpers.ConcatenateUriSegments(parentDescriptor.EditLink.OriginalString, entityDescriptorData.ParentPropertyForInsert), UriKind.RelativeOrAbsolute);
                    if (!insertUri.IsAbsoluteUri && contextData.BaseUri != null)
                    {
                        insertUri = new Uri(contextData.BaseUri, insertUri);
                    }
                }
            }

            return insertUri;
        }

        internal static Uri BuildLinkUri(LinkDescriptorData linkDescriptorData, LinkInfoData info)
        {
            Uri linkUri;
            if (info != null && info.RelationshipLink != null)
            {
                linkUri = info.RelationshipLink;
            }
            else
            {
                ExceptionUtilities.CheckObjectNotNull(linkDescriptorData.SourceDescriptor.EditLink, "Edit link of source descriptor cannot be null");
                linkUri = new Uri(UriHelpers.ConcatenateUriSegments(linkDescriptorData.SourceDescriptor.EditLink.OriginalString, Endpoints.Ref, linkDescriptorData.SourcePropertyName), UriKind.RelativeOrAbsolute);
            }

            return linkUri;
        }

        private static string ToClientHeaderFormat(DataServiceProtocolVersion version)
        {
            return version.ConvertToHeaderFormat() + ";" + HttpHeaders.NetFx;
        }

        private static void SetContentTypeHeaderForStream(ExpectedClientRequest request, StreamDescriptorData streamDescriptorData)
        {
            request.Headers[HttpHeaders.ContentType] = streamDescriptorData.ContentType;
            if (streamDescriptorData.ContentType == null)
            {
                request.Headers[HttpHeaders.ContentType] = MimeTypes.Any;
            }

            foreach (var header in streamDescriptorData.SaveStream.Headers)
            {
                request.Headers[header.Key] = header.Value;
            }

            request.Headers[HttpHeaders.Accept] = DefaultAccept;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "options", Justification = "Parameter is used in Silverlight build only")]
        private void SetDefaultAcceptHeader(ExpectedClientRequest request, SaveChangesOptions options)
        {
            request.Headers[HttpHeaders.Accept] = string.IsNullOrWhiteSpace(this.ClientRequestAcceptHeader) ? DefaultAccept : this.ClientRequestAcceptHeader;
            request.Headers[HttpHeaders.AcceptCharset] = DefaultEncoding;
        }

        private void SetContentTypeHeaderForEntity(ExpectedClientRequest request)
        {
            request.Headers[HttpHeaders.ContentType] = string.IsNullOrWhiteSpace(this.ClientRequestAcceptHeader) ? MimeTypes.ApplicationAtomXml : this.ClientRequestAcceptHeader;
        }

        private ExpectedClientRequest CreateLinkRequest(LinkDescriptorData linkDescriptorData, SaveChangesOptions options)
        {
            var info = linkDescriptorData.SourceDescriptor.LinkInfos.SingleOrDefault(l => l.Name == linkDescriptorData.SourcePropertyName);
            ExpectedClientRequest request = new ExpectedClientRequest() { Uri = BuildLinkUri(linkDescriptorData, info) };

            if (linkDescriptorData.State == EntityStates.Added)
            {
                request.Verb = HttpVerb.Post;

                // note: the edit-link is used rather than identity because the server needs to be able to query for the target entity
                // and the identity may not be an actual uri
                request.Body = new DeferredLink() { UriString = linkDescriptorData.TargetDescriptor.EditLink.OriginalString };
            }
            else if (linkDescriptorData.State == EntityStates.Modified)
            {
                if (linkDescriptorData.TargetDescriptor == null)
                {
                    request.Verb = HttpVerb.Delete;
                }
                else
                {
                    request.Verb = HttpVerb.Put;

                    // note: the edit-link is used rather than identity because the server needs to be able to query for the target entity
                    // and the identity may not be an actual uri
                    request.Body = new DeferredLink() { UriString = linkDescriptorData.TargetDescriptor.EditLink.OriginalString };
                }
            }
            else
            {
                ExceptionUtilities.Assert(linkDescriptorData.State == EntityStates.Deleted, "Link descriptor was in unexpected state '{0}'", linkDescriptorData.State);

                string keyString = this.EntityDescriptorValueCalculator.CalculateEntityKey(linkDescriptorData.TargetDescriptor.Entity);
                request.Uri = new Uri(request.Uri.OriginalString + keyString);
                request.Verb = HttpVerb.Delete;
            }

            request.Headers[HttpHeaders.IfMatch] = null;
            request.Headers[HttpHeaders.Prefer] = null;
            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(DataServiceProtocolVersion.V4);

            this.SetDefaultAcceptHeader(request, options);

            if (request.Verb != HttpVerb.Delete)
            {
                request.Headers[HttpHeaders.ContentType] = string.IsNullOrWhiteSpace(this.ClientRequestAcceptHeader) ? MimeTypes.ApplicationXml : this.ClientRequestAcceptHeader;
            }
            else
            {
                request.Headers[HttpHeaders.ContentType] = null;
            }

            string hintString = @"Link\r\n{{\r\n  Descriptor = {0}\r\n  Options = {1}\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, linkDescriptorData, options);
            
            return request;
        }

        private ExpectedClientRequest CreateEntityInsertRequest(DataServiceContextData contextData, IDictionary<object, IEnumerable<NamedValue>> propertyValuesBeforeSave, EntityDescriptorData entityDescriptorData, SaveChangesOptions options)
        {
            ExceptionUtilities.Assert(!entityDescriptorData.IsMediaLinkEntry, "Can only be used for non media-link-entries");

            var insertUri = GetEntityInsertUri(contextData, entityDescriptorData);
           
            ExpectedClientRequest request = new ExpectedClientRequest() { Verb = HttpVerb.Post, Uri = insertUri };

            string preference = contextData.AddAndUpdateResponsePreference.ToHeaderValue();
            DataServiceProtocolVersion dsv = GetDataServiceVersion(HttpVerb.Post, preference);
            dsv = dsv.IncreaseVersionIfRequired(this.VersionCalculator.CalculateDataServiceVersion(entityDescriptorData, contextData.MaxProtocolVersion));

            var payload = this.BuildEntityPayload(contextData, propertyValuesBeforeSave, entityDescriptorData, dsv);
            request.Body = payload;

            this.AddFoldedLinksToEntityInsertPayload(contextData, entityDescriptorData, payload);

            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(dsv);
            request.Headers[HttpHeaders.IfMatch] = null;            
            request.Headers[HttpHeaders.Prefer] = preference;

            this.SetDefaultAcceptHeader(request, options);
            this.SetContentTypeHeaderForEntity(request);

            string hintString = @"Entity insert\r\n{{\r\n  Descriptor = {0}\r\n  Options = {1}\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, entityDescriptorData, options);
                        
            return request;
        }

        private void AddFoldedLinksToEntityInsertPayload(DataServiceContextData contextData, EntityDescriptorData entityDescriptorData, EntityInstance payload)
        {
            var entityType = this.ModelSchema.EntityTypes.Single(t => t.FullName == entityDescriptorData.EntityClrType.FullName);

            foreach (var linkDescriptor in contextData.LinkDescriptorsData.Where(l => l.SourceDescriptor == entityDescriptorData))
            {
                if (linkDescriptor.TargetDescriptor.State == EntityStates.Added)
                {
                    continue;
                }

                var navigationProperty = entityType.AllNavigationProperties.Single(n => n.Name == linkDescriptor.SourcePropertyName);

                string contentType = MimeTypes.ApplicationAtomXml + ";type=";
                if (navigationProperty.ToAssociationEnd.Multiplicity == EndMultiplicity.Many)
                {
                    contentType += "feed";
                }
                else
                {
                    contentType += "entry";
                }

                // note: the edit-link is used rather than identity because the server needs to be able to query for the target entity
                // and the identity may not be an actual uri
                var link = new DeferredLink() { UriString = linkDescriptor.TargetDescriptor.EditLink.OriginalString }
                    .WithContentType(contentType).WithTitleAttribute(linkDescriptor.SourcePropertyName);

                payload.Add(new NavigationPropertyInstance(linkDescriptor.SourcePropertyName, link));
            }
        }

        private ExpectedClientRequest CreateEntityDeleteRequest(EntityDescriptorData entityDescriptorData, SaveChangesOptions options)
        {
            var request = new ExpectedClientRequest()
            {
                Verb = HttpVerb.Delete,
                Uri = entityDescriptorData.EditLink,
            };

            request.Headers[HttpHeaders.IfMatch] = entityDescriptorData.ETag;
            request.Headers[HttpHeaders.Prefer] = null;
            
            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(DataServiceProtocolVersion.V4);

            this.SetDefaultAcceptHeader(request, options);

            request.Headers[HttpHeaders.ContentType] = null;

            string hintString = @"Entity delete\r\n{{\r\n  Descriptor = {0}\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, entityDescriptorData);

            return request;
        }

        private ExpectedClientRequest CreateEntityUpdateRequest(DataServiceContextData contextData, IDictionary<object, IEnumerable<NamedValue>> propertyValuesBeforeSave, EntityDescriptorData entityDescriptorData, SaveChangesOptions options)
        {
            var request = new ExpectedClientRequest()
            {
                Verb = GetUpdateVerb(options),
                Uri = entityDescriptorData.EditLink,
            };

            string preference = contextData.AddAndUpdateResponsePreference.ToHeaderValue();
            var dsv = GetDataServiceVersion(request.Verb, preference);
            dsv = dsv.IncreaseVersionIfRequired(this.VersionCalculator.CalculateDataServiceVersion(entityDescriptorData, contextData.MaxProtocolVersion));

            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(dsv);
            request.Headers[HttpHeaders.IfMatch] = entityDescriptorData.ETag;
            request.Headers[HttpHeaders.Prefer] = preference;

            this.SetDefaultAcceptHeader(request, options);
            this.SetContentTypeHeaderForEntity(request);

            request.Body = this.BuildEntityPayload(contextData, propertyValuesBeforeSave, entityDescriptorData, dsv);

            string hintString = @"Entity update\r\n{{\r\n  Descriptor = {0}\r\n  Options = {1}\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, entityDescriptorData, options);

            return request;
        }

        private ExpectedClientRequest CreateStreamInsertRequest(DataServiceContextData contextData, StreamDescriptorData streamDescriptorData, SaveChangesOptions options)
        {
            ExceptionUtilities.Assert(streamDescriptorData.Name == null, "Can only be used for media-resources");

            var insertUri = GetEntityInsertUri(contextData, streamDescriptorData.EntityDescriptor);

            var request = this.CreateSaveStreamRequest(streamDescriptorData, insertUri);
            
            string preference = contextData.AddAndUpdateResponsePreference.ToHeaderValue();
            var dsv = GetDataServiceVersion(request.Verb, preference);

            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(dsv);
            request.Headers[HttpHeaders.IfMatch] = null;
            request.Headers[HttpHeaders.Prefer] = preference;

            this.SetDefaultAcceptHeader(request, options);
            SetContentTypeHeaderForStream(request, streamDescriptorData);

            string hintString = @"Stream insert\r\n{{\r\n  Descriptor = {0}\r\n\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, streamDescriptorData);

            return request;
        }
        
        private ExpectedClientRequest CreateStreamUpdateRequest(StreamDescriptorData streamDescriptorData, SaveChangesOptions options)
        {
            var request = this.CreateSaveStreamRequest(streamDescriptorData, streamDescriptorData.EditLink);
            request.Headers[HttpHeaders.IfMatch] = streamDescriptorData.ETag;
            request.Headers[HttpHeaders.Prefer] = null;
            request.Headers[HttpHeaders.DataServiceVersion] = ToClientHeaderFormat(DataServiceProtocolVersion.V4);
            this.SetDefaultAcceptHeader(request, options);
            SetContentTypeHeaderForStream(request, streamDescriptorData);

            string hintString = @"Stream update\r\n{{\r\n  Descriptor = {0}\r\n\r\n}}";
            request.DebugHintString = string.Format(CultureInfo.InvariantCulture, hintString, streamDescriptorData);

            return request;
        }

        private ExpectedClientRequest CreateSaveStreamRequest(StreamDescriptorData streamDescriptorData, Uri requestUri)
        {
            HttpVerb verb;
            if (streamDescriptorData.State == EntityStates.Added)
            {
                verb = HttpVerb.Post;
            }
            else
            {
                ExceptionUtilities.Assert(streamDescriptorData.State == EntityStates.Modified, "Stream descriptor state should only be added or modified. State was: '{0}'", streamDescriptorData.State);
                verb = HttpVerb.Put;
            }

            var request = new ExpectedClientRequest() { Verb = verb, Uri = requestUri };
            foreach (var header in streamDescriptorData.SaveStream.Headers)
            {
                request.Headers.Add(header);
            }

            request.Body = new PrimitiveValue(null, streamDescriptorData.SaveStream.StreamLogger.GetAllBytesRead());
            return request;
        }

        private EntityInstance BuildEntityPayload(
            DataServiceContextData contextData, 
            IDictionary<object, IEnumerable<NamedValue>> propertyValuesBeforeSave, 
            EntityDescriptorData entityDescriptorData, 
            DataServiceProtocolVersion dsv)
        {
            IEnumerable<NamedValue> propertyValues;
            ExceptionUtilities.Assert(propertyValuesBeforeSave.TryGetValue(entityDescriptorData.Entity, out propertyValues), "Could not find property values for descriptor: {0}", entityDescriptorData);

            var entityType = this.ModelSchema.EntityTypes.Single(t => t.FullName == entityDescriptorData.EntityClrType.FullName);
            var entityInstance = this.PayloadBuilder.EntityInstance(entityType, propertyValues);

            new ExpectedPayloadNormalizer(contextData, dsv).Normalize(entityInstance, entityDescriptorData);

            return entityInstance;
        }

        /// <summary>
        /// Helper class for normalizing the payloads generated by the test framework to match those produced by the client
        /// </summary>
        internal class ExpectedPayloadNormalizer : ODataPayloadElementVisitorBase
        {
            private readonly DataServiceContextData contextData;
            private readonly DataServiceProtocolVersion dsv;
            private readonly Stack<Type> typeStack = new Stack<Type>();

            /// <summary>
            /// Initializes a new instance of the ExpectedPayloadNormalizer class
            /// </summary>
            /// <param name="contextData">The context data</param>
            /// <param name="dsv">The data service version</param>
            public ExpectedPayloadNormalizer(DataServiceContextData contextData, DataServiceProtocolVersion dsv)
            {
                ExceptionUtilities.CheckArgumentNotNull(contextData, "contextData");
                this.contextData = contextData;
                this.dsv = dsv;
            }

            /// <summary>
            /// Gets the type stack used for invoking the type resolver on the context. Unit test hook only.
            /// </summary>
            internal Stack<Type> TypeStack
            {
                get { return this.typeStack; }
            }

            /// <summary>
            /// Normalizes the given entity payload element
            /// </summary>
            /// <param name="entityPayload">The entity payload element</param>
            /// <param name="entityDescriptorData">The descriptor data for the entity</param>
            public void Normalize(EntityInstance entityPayload, EntityDescriptorData entityDescriptorData)
            {
                ExceptionUtilities.CheckArgumentNotNull(entityPayload, "entityPayload");
                ExceptionUtilities.CheckArgumentNotNull(entityDescriptorData, "entityDescriptorData");
                this.typeStack.Push(entityDescriptorData.EntityClrType);

                // do this before recursing because it could be over-written by the visit method
                entityPayload.FullTypeName = entityDescriptorData.ServerTypeName;
                
                entityPayload.Id = string.Empty;
                if (entityDescriptorData.Identity != null)
                {
                    entityPayload.Id = entityDescriptorData.Identity.OriginalString;
                }

                entityPayload.Accept(this);
            }

            /// <summary>
            /// Visits a complex instance, clears its type name, and sorts/normalizes its properties
            /// </summary>
            /// <param name="payloadElement">The complex instance</param>
            public override void Visit(ComplexInstance payloadElement)
            {
                base.Visit(payloadElement);

                payloadElement.FullTypeName = null;
                if (this.contextData.ResolveName != null && !payloadElement.IsNull)
                {
                    payloadElement.FullTypeName = this.contextData.ResolveName(this.typeStack.Peek());
                }

                this.SortAndNormalizeProperties(payloadElement);
            }

            /// <summary>
            /// Visits a complex multivalue property and clears its type name.
            /// Note: this has to be done at the property level rather than just at the multivalue in order to maintain the CLR type stack.
            /// </summary>
            /// <param name="payloadElement">The complex multivalue property</param>
            public override void Visit(ComplexMultiValueProperty payloadElement)
            {
                this.VisitProperty(
                    payloadElement,
                    payloadElement.Value,
                    () =>
                    {
                        payloadElement.Value.FullTypeName = null;
                        if (this.contextData.ResolveName != null)
                        {
                            string elementName = this.contextData.ResolveName(this.typeStack.Peek());
                            payloadElement.Value.FullTypeName = string.Concat(ODataConstants.BeginMultiValueTypeIdentifier, elementName, ODataConstants.EndMultiValueTypeNameIdentifier);
                        }

                        payloadElement.Value.ForEach(v => v.FullTypeName = null);
                    });
            }

            /// <summary>
            /// Visits an entity instance and fixes its annotations, type name, and property ordering/typing
            /// </summary>
            /// <param name="payloadElement">The entity instance</param>
            public override void Visit(EntityInstance payloadElement)
            {
                base.Visit(payloadElement);

                if (payloadElement.FullTypeName == null && this.contextData.ResolveName != null)
                {
                    payloadElement.FullTypeName = this.contextData.ResolveName(this.typeStack.Peek());
                }

                this.SortAndNormalizeProperties(payloadElement);

                payloadElement.WithContentType(MimeTypes.ApplicationXml);

                if (payloadElement.IsMediaLinkEntry())
                {
                    payloadElement.Annotations.RemoveAll(a => a is ContentTypeAnnotation);
                }

                // need to fix-up any places we've mapped an empty string to an element, as these are indistinguishable from null
                var epmQueue = new Queue<XmlTreeAnnotation>(payloadElement.Annotations.OfType<XmlTreeAnnotation>());
                while (epmQueue.Count > 0)
                {
                    var tree = epmQueue.Dequeue();
                    if (!tree.IsAttribute && string.IsNullOrEmpty(tree.PropertyValue))
                    {
                        tree.PropertyValue = null;
                    }

                    tree.Children.ForEach(c => epmQueue.Enqueue(c));
                }
            }

            /// <summary>
            /// Visits a primitive multivalue property and clears its type name
            /// Note: this has to be done at the property level rather than just at the multivalue in order to maintain the CLR type stack.
            /// </summary>
            /// <param name="payloadElement">The primitive multivalue property</param>
            public override void Visit(PrimitiveMultiValueProperty payloadElement)
            {
                this.VisitProperty(
                    payloadElement,
                    payloadElement.Value,
                    () =>
                    {
                        payloadElement.Value.ForEach(v => v.FullTypeName = null);
                    });
            }

            /// <summary>
            /// Visits a primitive value and clears its type name if it is 'Edm.String'
            /// </summary>
            /// <param name="payloadElement">The primitive value</param>
            public override void Visit(PrimitiveValue payloadElement)
            {
                base.Visit(payloadElement);

                if (payloadElement.FullTypeName == "Edm.String")
                {
                    payloadElement.FullTypeName = null;
                }
            }

            /// <summary>
            /// Visits a property
            /// </summary>
            /// <param name="payloadElement">The property to visit</param>
            /// <param name="value">The value of the property</param>
            protected override void VisitProperty(PropertyInstance payloadElement, ODataPayloadElement value)
            {
                this.VisitProperty(payloadElement, value, null);
            }

            private void VisitProperty(PropertyInstance payloadElement, ODataPayloadElement value, Action action)
            {
                ExceptionUtilities.CheckArgumentNotNull(payloadElement, "payloadElement");

                try
                {
                    var currentType = this.typeStack.Peek();
                    ExceptionUtilities.CheckObjectNotNull(currentType, "Current type should not be null");

                    var propertyInfo = currentType.GetProperty(payloadElement.Name);
                    ExceptionUtilities.CheckObjectNotNull(propertyInfo, "Could not find property '{0}' on type '{1}'", payloadElement.Name, currentType);

                    var genericCollectionType = propertyInfo.PropertyType.GetInterfaces().Where(t => t.IsGenericType()).SingleOrDefault(t => typeof(ICollection<>) == t.GetGenericTypeDefinition());
                    if (genericCollectionType != null)
                    {
                        this.typeStack.Push(genericCollectionType.GetGenericArguments()[0]);
                    }
                    else
                    {
                        this.typeStack.Push(propertyInfo.PropertyType);
                    }

                    base.VisitProperty(payloadElement, value);

                    if (action != null)
                    {
                        action();
                    }
                }
                finally
                {
                    this.typeStack.Pop();
                }
            }

            /// <summary>
            /// Because the test deserializer used to parse the actual payload will not have metadata, we fix it up here.
            /// Note: this should probably move to happen just before comparison, rather than when generating expectations.
            /// </summary>
            /// <param name="payloadElement">The payload element</param>
            private void SortAndNormalizeProperties(ComplexInstance payloadElement)
            {
                var sortedProperties = payloadElement.Properties.OrderBy(p => p.Name).ToList();
                foreach (var p in sortedProperties)
                {
                    var property = p;
                    payloadElement.Remove(property);

                    // If we have a primitive property with null value, we write a type name for the null value
                    // in V1 and V2. If not type name is available or we are in V3, we don't do that and thus expect a null property instance.
                    var primitiveProperty = property as PrimitiveProperty;
                    if (primitiveProperty != null && primitiveProperty.Value.IsNull && 
                        (this.dsv >= DataServiceProtocolVersion.V4 || string.IsNullOrEmpty(primitiveProperty.Value.FullTypeName)))
                    {
                        property = new NullPropertyInstance(property.Name, null).WithAnnotations(property.Annotations);
                    }

                    var complexProperty = property as ComplexProperty;
                    if (complexProperty != null && complexProperty.Value.IsNull)
                    {
                        property = new NullPropertyInstance(property.Name, complexProperty.Value.FullTypeName).WithAnnotations(property.Annotations);
                    }

                    var complexCollectionProperty = property as ComplexMultiValueProperty;
                    if (complexCollectionProperty != null && complexCollectionProperty.Value.Count == 0 && complexCollectionProperty.Value.FullTypeName == null)
                    {
                        property = new PrimitiveProperty(complexCollectionProperty.Name, complexCollectionProperty.Value.FullTypeName, string.Empty);
                    }

                    payloadElement.Add(property);
                }
            }
        }
    }
}
